/**
 * A numeric text field that provides automatic keystroke filtering to disallow non-numeric
 * characters, and numeric validation to limit the value to a range of valid numbers. The range
 * of acceptable number values can be controlled by setting the {@link #minValue} and
 * {@link #maxValue} configs, and fractional decimals can be disallowed by setting
 * {@link #allowDecimals} to `false`.
 *
 * By default, the number field is also rendered with a set of up/down spinner buttons and has
 * up/down arrow key and mouse wheel event listeners attached for incrementing/decrementing
 * the value by the {@link #step} value. To hide the spinner buttons set
 * `{@link #hideTrigger hideTrigger}:true`; to disable the arrow key and mouse wheel handlers set
 * `{@link #keyNavEnabled keyNavEnabled}:false` and
 * `{@link #mouseWheelEnabled mouseWheelEnabled}:false`. See the example below.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'On The Wall',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'bottles',
 *             fieldLabel: 'Bottles of Beer',
 *             value: 99,
 *             maxValue: 99,
 *             minValue: 0
 *         }],
 *         buttons: [{
 *             text: 'Take one down, pass it around',
 *             handler: function() {
 *                 this.up('form').down('[name=bottles]').spinDown();
 *             }
 *         }]
 *     });
 *
 * # Removing UI Enhancements
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Personal Info',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'age',
 *             fieldLabel: 'Age',
 *             minValue: 0, //prevents negative numbers
 *
 *             // Remove spinner buttons, and arrow key and mouse wheel listeners
 *             hideTrigger: true,
 *             keyNavEnabled: false,
 *             mouseWheelEnabled: false
 *         }]
 *     });
 *
 * # Using Step
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Step',
 *         width: 300,
 *         bodyPadding: 10,
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'evens',
 *             fieldLabel: 'Even Numbers',
 *
 *             // Set step so it skips every other number
 *             step: 2,
 *             value: 0,
 *
 *             // Add change handler to force user-entered numbers to evens
 *             listeners: {
 *                 change: function(field, value) {
 *                     value = parseInt(value, 10);
 *                     field.setValue(value + value % 2);
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.form.field.Number', {
    extend: 'Ext.form.field.Spinner',
    alias: 'widget.numberfield',
    alternateClassName: ['Ext.form.NumberField', 'Ext.form.Number'],

    /**
     * @cfg {RegExp} stripCharsRe
     * @private
     */
    /**
     * @cfg {RegExp} maskRe
     * @private
     */

    /**
     * @cfg {Boolean} [allowExponential=true]
     * Set to `false` to disallow Exponential number notation
     */
    allowExponential: true,

    /**
     * @cfg {Boolean} [allowDecimals=true]
     * False to disallow decimal values
     */
    allowDecimals: true,

    /**
     * @cfg {String} decimalSeparator
     * Character(s) to allow as the decimal separator.
     * Defaults to {@link Ext.util.Format#decimalSeparator decimalSeparator}.
     * @locale
     */
    decimalSeparator: null,

    /**
     * @cfg {Boolean} [submitLocaleSeparator=true]
     * False to ensure that the {@link #getSubmitValue} method strips
     * always uses `.` as the separator, regardless of the {@link #decimalSeparator}
     * configuration.
     * @locale
     */
    submitLocaleSeparator: true,

    /**
     * @cfg {Number} decimalPrecision
     * The maximum precision to display after the decimal separator
     * @locale
     */
    decimalPrecision: 2,

    /**
     * @cfg {Number} minValue
     * The minimum allowed value. Will be used by the field's validation logic,
     * and for {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling
     * the down spinner button}.
     *
     * Defaults to Number.NEGATIVE_INFINITY.
     */
    minValue: Number.NEGATIVE_INFINITY,

    /**
     * @cfg {Number} maxValue
     * The maximum allowed value. Will be used by the field's validation logic, and for
     * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the up spinner button}.
     *
     * Defaults to Number.MAX_VALUE.
     */
    maxValue: Number.MAX_VALUE,

    /**
     * @cfg {Number} step
     * Specifies a numeric interval by which the field's value will be incremented or decremented
     * when the user invokes the spinner.
     */
    step: 1,

    /**
     * @cfg {String} minText
     * Error text to display if the minimum value validation fails.
     * @locale
     */
    minText: 'The minimum value for this field is {0}',

    /**
     * @cfg {String} maxText
     * Error text to display if the maximum value validation fails.
     * @locale
     */
    maxText: 'The maximum value for this field is {0}',

    /**
     * @cfg {String} nanText
     * Error text to display if the value is not a valid number. For example, this can happen
     * if a valid character like '.' or '-' is left in the field with no number.
     * @locale
     */
    nanText: '{0} is not a valid number',

    /**
     * @cfg {String} negativeText
     * Error text to display if the value is negative and {@link #minValue} is set to 0.
     * This is used instead of the {@link #minText} in that circumstance only.
     * @locale
     */
    negativeText: 'The value cannot be negative',

    /**
     * @cfg {String} baseChars
     * The base set of characters to evaluate as valid numbers.
     */
    baseChars: '0123456789',

    /**
     * @cfg {Boolean} autoStripChars
     * True to automatically strip not allowed characters from the field.
     */
    autoStripChars: false,

    initComponent: function() {
        var me = this;

        if (me.decimalSeparator === null) {
            me.decimalSeparator = Ext.util.Format.decimalSeparator;
        }

        me.callParent();

        me.setMinValue(me.minValue);
        me.setMaxValue(me.maxValue);
    },

    getSubTplData: function(fieldData) {
        var me = this,
            min = me.minValue,
            max = me.maxValue,
            data, inputElAttr, value;

        data = me.callParent([fieldData]);

        inputElAttr = data.inputElAriaAttributes;

        if (inputElAttr) {
            // The checks are to skip the default min and max values,
            // in which case we don't want to set corresponding ARIA
            // attributes at all
            if (min > Number.NEGATIVE_INFINITY) {
                inputElAttr['aria-valuemin'] = min;
            }

            if (max < Number.MAX_VALUE) {
                inputElAttr['aria-valuemax'] = max;
            }

            value = me.getValue();

            if (value != null && value >= min && value <= max) {
                inputElAttr['aria-valuenow'] = value;
            }
        }

        return data;
    },

    setValue: function(value) {
        var me = this,
            bind, valueBind;

        // This portion of the code is to prevent a binding from stomping over
        // the typed value. Say we have decimalPrecision 4 and the user types
        // 1.23456. The value of the field will be set as 1.2346 and published to
        // the viewmodel, which will trigger the binding to fire and setValue to
        // be called on the field, which would then set the value (and rawValue) to
        // 1.2346. Instead, if we have focus and the value is the same, just leave
        // the rawValue alone
        if (me.hasFocus) {
            bind = me.getBind();
            valueBind = bind && bind.value;

            if (valueBind && valueBind.syncing && value === me.value) {
                return me;
            }
        }

        return me.callParent([value]);
    },

    /**
     * Runs all of Number's validations and returns an array of any errors. Note that this first
     * runs Text's validations, so the returned array is an amalgamation of all field errors.
     * The additional validations run test that the value is a number, and that it is within
     * the configured min and max values.
     * @param {Object} [value] The value to get errors for (defaults to the current field value)
     * @return {String[]} All validation errors for this field
     */
    getErrors: function(value) {
        value = arguments.length > 0 ? value : this.processRawValue(this.getRawValue());

        // eslint-disable-next-line vars-on-top
        var me = this,
            errors = me.callParent([value]),
            format = Ext.String.format,
            num;

        if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
            return errors;
        }

        value = String(value).replace(me.decimalSeparator, '.');

        if (isNaN(value)) {
            errors.push(format(me.nanText, value));
        }

        num = me.parseValue(value);

        if (me.minValue === 0 && num < 0) {
            errors.push(this.negativeText);
        }
        else if (num < me.minValue) {
            errors.push(format(me.minText, me.minValue));
        }

        if (num > me.maxValue) {
            errors.push(format(me.maxText, me.maxValue));
        }

        return errors;
    },

    rawToValue: function(rawValue) {
        var value = this.fixPrecision(this.parseValue(rawValue));

        if (value === null) {
            value = rawValue || null;
        }

        return value;
    },

    valueToRaw: function(value) {
        var me = this,
            decimalSeparator = me.decimalSeparator;

        value = me.parseValue(value);
        value = me.fixPrecision(value);
        value =
            Ext.isNumber(value) ? value : parseFloat(String(value).replace(decimalSeparator, '.'));
        value = isNaN(value) ? '' : String(value).replace('.', decimalSeparator);

        return value;
    },

    getSubmitValue: function() {
        var me = this,
            value = me.callParent();

        if (!me.submitLocaleSeparator) {
            value = value.replace(me.decimalSeparator, '.');
        }

        return value;
    },

    onChange: function(newValue) {
        var ariaDom = this.ariaEl.dom;

        this.toggleSpinners();
        this.callParent(arguments);

        if (ariaDom) {
            if (Ext.isNumber(newValue) && isFinite(newValue)) {
                ariaDom.setAttribute('aria-valuenow', newValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuenow');
            }
        }

    },

    toggleSpinners: function() {
        var me = this,
            value = me.getValue(),
            valueIsNull = value === null,
            enabled;

        // If it's disabled, only allow it to be re-enabled if we are
        // the ones who are disabling it.
        if (me.spinUpEnabled || me.spinUpDisabledByToggle) {
            enabled = valueIsNull || value < me.maxValue;
            me.setSpinUpEnabled(enabled, true);
        }

        if (me.spinDownEnabled || me.spinDownDisabledByToggle) {
            enabled = valueIsNull || value > me.minValue;
            me.setSpinDownEnabled(enabled, true);
        }
    },

    /**
     * Replaces any existing {@link #minValue} with the new value.
     * @param {Number} value The minimum value
     */
    setMinValue: function(value) {
        var me = this,
            ariaDom = me.ariaEl.dom,
            minValue, allowed;

        me.minValue = minValue = Ext.Number.from(value, Number.NEGATIVE_INFINITY);
        me.toggleSpinners();

        // May not be rendered yet
        if (ariaDom) {
            if (minValue > Number.NEGATIVE_INFINITY) {
                ariaDom.setAttribute('aria-valuemin', minValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuemin');
            }
        }

        // Build regexes for masking and stripping based on the configured options
        if (me.disableKeyFilter !== true) {
            allowed = me.baseChars + '';

            if (me.allowExponential) {
                allowed += me.decimalSeparator + 'e+-';
            }
            else {
                if (me.allowDecimals) {
                    allowed += me.decimalSeparator;
                }

                if (me.minValue < 0) {
                    allowed += '-';
                }
            }

            allowed = Ext.String.escapeRegex(allowed);
            me.maskRe = new RegExp('[' + allowed + ']');

            if (me.autoStripChars) {
                me.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi');
            }
        }
    },

    /**
     * Replaces any existing {@link #maxValue} with the new value.
     * @param {Number} value The maximum value
     */
    setMaxValue: function(value) {
        var ariaDom = this.ariaEl.dom,
            maxValue;

        this.maxValue = maxValue = Ext.Number.from(value, Number.MAX_VALUE);

        // May not be rendered yet
        if (ariaDom) {
            if (maxValue < Number.MAX_VALUE) {
                ariaDom.setAttribute('aria-valuemax', maxValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuemax');
            }
        }

        this.toggleSpinners();
    },

    /**
     * @private
     */
    parseValue: function(value) {
        value = parseFloat(String(value).replace(this.decimalSeparator, '.'));

        return isNaN(value) ? null : value;
    },

    /**
     * @private
     */
    fixPrecision: function(value) {
        var me = this,
            nan = isNaN(value),
            precision = me.decimalPrecision;

        if (nan || !value) {
            return nan ? '' : value;
        }
        else if (!me.allowDecimals || precision <= 0) {
            precision = 0;
        }

        return parseFloat(Ext.Number.toFixed(parseFloat(value), precision));
    },

    onBlur: function(e) {
        var me = this,
            v = me.rawToValue(me.getRawValue());

        if (!Ext.isEmpty(v)) {
            me.setValue(v);
        }

        me.callParent([e]);
    },

    setSpinUpEnabled: function(enabled, internal) {
        this.callParent(arguments);

        if (!internal) {
            delete this.spinUpDisabledByToggle;
        }
        else {
            this.spinUpDisabledByToggle = !enabled;
        }
    },

    onSpinUp: function() {
        var me = this;

        if (!me.readOnly) {
            me.setSpinValue(
                Ext.Number.constrain(me.getValue() + me.step, me.minValue, me.maxValue)
            );
        }
    },

    setSpinDownEnabled: function(enabled, internal) {
        this.callParent(arguments);

        if (!internal) {
            delete this.spinDownDisabledByToggle;
        }
        else {
            this.spinDownDisabledByToggle = !enabled;
        }
    },

    onSpinDown: function() {
        var me = this;

        if (!me.readOnly) {
            me.setSpinValue(
                Ext.Number.constrain(me.getValue() - me.step, me.minValue, me.maxValue)
            );
        }
    },

    setSpinValue: function(value) {
        var me = this;

        if (me.enforceMaxLength) {
            // We need to round the value here, otherwise we could end up with a
            // very long number (think 0.1 + 0.2)
            if (me.fixPrecision(value).toString().length > me.maxLength) {
                return;
            }
        }

        me.setValue(value);
    }
});
